package cn.iocoder.yudao.webapp;

import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.NumberUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import com.github.fppt.jedismock.RedisServer;
import jakarta.annotation.Nonnull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.quartz.impl.SchedulerRepository;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.beans.BeansException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.util.StopWatch;
import org.springframework.web.context.support.ServletRequestHandledEvent;

import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static cn.iocoder.yudao.framework.constant.ConfigConstants.SYSTEM_USER_DIR;

/**
 * <pre>
 * OOoO0OOoO0OOOooo0oOOOO0OOOOO0oooOO0ooOOO0Ooooo0OOOOo0ooooO0OOooo0Ooooo0OOOOO
 *    ┏┓   ┏┓
 *   ┏┛┻━━━┛┻┓
 *   ┃       ┃
 *   ┃   ━   ┃
 *   ┃ ┳┛ ┗┳ ┃
 *   ┃       ┃
 *   ┃   ┻   ┃
 *   ┃       ┃
 *   ┗━┓   ┏━┛
 *     ┃   ┃ 基于芋道源码深度二次开发的管理系统
 *     ┃   ┃ 技术支持：山野羡民（1032694760@qq.com）
 *     ┃   ┗━━━┓
 *     ┃       ┣┓
 *     ┃       ┏┛
 *     ┗┓┓┏━┳┓┏┛
 *      ┃┫┫ ┃┫┫
 *      ┗┻┛ ┗┻┛
 * OOoO0OOoO0OOOooo0oOOOO0OOOOO0oooOO0ooOOO0Ooooo0OOOOo0ooooO0OOooo0Ooooo0OOOOO
 * </pre>
 */
@Slf4j
@SpringBootApplication(
        // 扫描未通过【自动配置】加载而需要加载的有【@Component@Controller@Service@Configuration@Repository】等注解的基础包
        scanBasePackages = {
                "${yudao.info.base-package}.module",
        }
)
public class MainApplication extends SpringBootServletInitializer implements ApplicationContextAware, SmartApplicationListener {
    private static volatile ConfigurableApplicationContext applicationContext;
    private static RedisServer redisServer;

    static {
        // Hibernate日志输出到SLF4J，参阅 https://www.cnblogs.com/tankaixiong/p/html
        System.setProperty("org.jboss.logging.provider", "slf4j");
        // JUL日志转为SLF4J输出，参阅 https://blog.csdn.net/csdlwzy/article/details/88035393
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();

    }

    /**
     * 注：这里只是 jar 包入口， war 包部署运行并不会走这个方法
     */
    public static void main(String[] args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // [--spring.profiles.active=dev, --xianmin.mock.redis=false]
        log.info("Args: {}", Arrays.deepToString(args));
        for (String arg : args) {
            if (arg == null || !arg.contains("=")) {
                continue;
            }
            String[] argKV = arg.split("=");
            if ("--xianmin.mock.redis".equalsIgnoreCase(argKV[0]) && NumberUtil.isNumber(argKV[1])) {
                // 在当前主机上模拟 Redis 服务来实现不依赖于真实的 Redis 的运行
                try {
                    redisServer = new RedisServer(NumberUtil.parseInt(argKV[1]));
                    redisServer.start();
                    log.info("【REDIS MOCK SERVER】 start succeed: {}/{}", redisServer.getHost(), redisServer.getBindPort());
                } catch (Exception e) {
                    log.error("【REDIS MOCK SERVER】 start failed", e);
                }
                break;
            }
        }
        applicationContext = SpringApplication.run(MainApplication.class, args);
        SpringUtils.setApplicationContext(applicationContext);
        stopWatch.stop();
        ThreadUtil.execute(() -> {
            ThreadUtil.sleep(2, TimeUnit.SECONDS); // 延迟 1 秒，保证输出到结尾
            String applicationName = SpringUtils.getApplicationName();
            System.out.print("------------------------------------------------------------------------\n");
            System.out.print("App Name: " + applicationName + "\n");
            System.out.print("Work Dir: " + SYSTEM_USER_DIR + "\n");
            System.out.print("Boot Spent: " + stopWatch.getTotalTimeSeconds() + "s\n");
            System.out.print("------------------------------------------------------------------------\n");
        });
    }

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext ctx) throws BeansException {
        if (ctx instanceof ConfigurableApplicationContext context) {
            applicationContext = context;
            SpringUtils.setApplicationContext(context);
        }
    }

    /**
     * 注：这里是用于适配 war 包
     */
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        // See https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.spring-application.startup-tracking
        application.applicationStartup(new BufferingApplicationStartup(2048));
        // 打 WAR 包的配置
        return application.sources(MainApplication.class);
    }

    @Override
    public boolean supportsEventType(@Nonnull Class<? extends
            ApplicationEvent> eventType) {
        return true;
    }

    @SneakyThrows
    @Override
    public void onApplicationEvent(@Nonnull ApplicationEvent event) {
        // ServletRequestHandledEvent 大约一秒一次，太频繁了，不打印它的日志
        if (!(event instanceof ServletRequestHandledEvent)) {
            log.debug("onApplicationEvent: {}", event);
        }
        if (event instanceof ApplicationStartedEvent applicationStartedEvent) {
            applicationContext = applicationStartedEvent.getApplicationContext();
            SpringUtils.setApplicationContext(applicationContext);
            ThreadUtil.execute(() -> {
                ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒，保证输出到结尾
                SpringUtils.printHost(applicationStartedEvent);
            });
        }
        if (event instanceof ContextClosedEvent) {
            // WAR 包部署模式，Tomcat 启动停止慢问题查找解决：https://blog.csdn.net/qq924862077/article/details/79411400
            // 通过查看 Tomcat 日志 catalina.xxx.log，在关闭 Tomcat 时会清除掉引用线程，但是无法停止线程，可能会导致内存泄漏。
            // 通过日志查看多个应用中都出现了线程无法停止的异常信息。
            // 结论：Tomcat 会等待应用的线程关闭之后才会将停止，由于众多线程无法正常停止，导致 Tomcat 停止时间过长。
            // 解决方法：服务关闭时要手动关闭服务创建的线程及线程池。
            stopRedisMockServer();
            destroyThreads();
        }
    }

    private void destroyThreads() {
        final Set<Thread> threads = Thread.getAllStackTraces().keySet();
        for (Thread thread : threads) {
            synchronized (this) {
                try {
                    thread.interrupt();
                    log.info("Thread[{}] interrupt success", thread.getName());
                } catch (Exception e) {
                    log.warn("Thread[{}] interrupt error", thread.getName());
                }
            }
        }
        SchedulerRepository.getInstance().lookupAll().forEach(scheduler -> {
            try {
                scheduler.shutdown(false);
                log.info("Scheduler[{}] ofNotify quartz shutdown success", scheduler.getSchedulerName());
            } catch (SchedulerException e) {
                log.warn("Scheduler ofNotify quartz shutdown error", e);
            }
        });
    }

    private void stopRedisMockServer() {
        if (redisServer != null && redisServer.isRunning()) {
            try {
                redisServer.stop();
                log.info("【REDIS MOCK SERVER】 stop succeed: {}/{}", redisServer.getHost(), redisServer.getBindPort());
            } catch (Exception e) {
                log.warn("【REDIS MOCK SERVER】 stop failed");
                e.printStackTrace(System.err);
            }
            redisServer = null;
        }
    }

}
